/*
* This file is part of The Technic Launcher Version 3.
* Copyright ©2015 Syndicate, LLC
*
* The Technic Launcher is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Technic Launcher is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the Technic Launcher. If not, see <http://www.gnu.org/licenses/>.
*/
package net.technicpack.launcher.ui.components.modpacks;
import net.technicpack.contrib.romainguy.AbstractFilter;
import net.technicpack.launcher.ui.controls.modpacks.FindMoreWidget;
import net.technicpack.launchercore.auth.IAuthListener;
import net.technicpack.launchercore.auth.IUserType;
import net.technicpack.launchercore.modpacks.*;
import net.technicpack.launchercore.modpacks.packinfo.CombinedPackInfo;
import net.technicpack.launchercore.modpacks.sources.IPackSource;
import net.technicpack.launchercore.modpacks.sources.NameFilterPackSource;
import net.technicpack.platform.IPlatformApi;
import net.technicpack.platform.IPlatformSearchApi;
import net.technicpack.platform.io.PlatformPackInfo;
import net.technicpack.platform.packsources.SearchResultPackSource;
import net.technicpack.platform.packsources.SinglePlatformSource;
import net.technicpack.rest.RestfulAPIException;
import net.technicpack.rest.io.PackInfo;
import net.technicpack.solder.ISolderApi;
import net.technicpack.solder.ISolderPackApi;
import net.technicpack.ui.controls.TintablePanel;
import net.technicpack.ui.controls.WatermarkTextField;
import net.technicpack.ui.controls.borders.RoundBorder;
import net.technicpack.ui.lang.IRelocalizableResource;
import net.technicpack.ui.lang.ResourceLoader;
import net.technicpack.launcher.ui.LauncherFrame;
import net.technicpack.ui.controls.list.SimpleScrollbarUI;
import net.technicpack.launcher.ui.controls.modpacks.ModpackWidget;
import net.technicpack.launchercore.image.ImageRepository;
import net.technicpack.utilslib.DesktopUtils;
import javax.swing.*;
import javax.swing.Timer;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ModpackSelector extends TintablePanel implements IModpackContainer, IAuthListener<IUserType>, IRelocalizableResource {
private ResourceLoader resources;
private PackLoader packLoader;
private IPackSource technicSolder;
private ImageRepository<ModpackModel> iconRepo;
private final IPlatformApi platformApi;
private final IPlatformSearchApi platformSearchApi;
private final ISolderApi solderApi;
private JPanel widgetList;
private JScrollPane scrollPane;
private ModpackInfoPanel modpackInfoPanel;
private LauncherFrame launcherFrame;
private JTextField filterContents;
private FindMoreWidget findMoreWidget;
private MemoryModpackContainer defaultPacks = new MemoryModpackContainer();
private Map<String, ModpackWidget> allModpacks = new HashMap<String, ModpackWidget>();
private ModpackWidget selectedWidget;
private PackLoadJob currentLoadJob;
private Timer currentSearchTimer;
private Pattern slugRegex;
private Pattern siteRegex;
private String lastFilterContents = "";
private String findMoreUrl;
private static final int MAX_SEARCH_STRING = 90;
public ModpackSelector(ResourceLoader resources, PackLoader packLoader, IPackSource techicSolder, ISolderApi solderApi, IPlatformApi platformApi, IPlatformSearchApi platformSearchApi, ImageRepository<ModpackModel> iconRepo) {
this.resources = resources;
this.packLoader = packLoader;
this.iconRepo = iconRepo;
this.technicSolder = techicSolder;
this.platformApi = platformApi;
this.solderApi = solderApi;
this.platformSearchApi = platformSearchApi;
slugRegex = Pattern.compile("^[a-zA-Z0-9-]+$");
siteRegex = Pattern.compile("^([a-zA-Z0-9-]+)\\.\\d+$");
findMoreWidget = new FindMoreWidget(resources);
findMoreWidget.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DesktopUtils.browseUrl(findMoreUrl);
}
});
relocalize(resources);
}
public void setInfoPanel(ModpackInfoPanel modpackInfoPanel) {
this.modpackInfoPanel = modpackInfoPanel;
}
public void setLauncherFrame(LauncherFrame launcherFrame) {
this.launcherFrame = launcherFrame;
}
public ModpackModel getSelectedPack() {
if (selectedWidget == null)
return null;
return selectedWidget.getModpack();
}
private void initComponents() {
setLayout(new BorderLayout());
setBackground(LauncherFrame.COLOR_SELECTOR_BACK);
setMaximumSize(new Dimension(287, getMaximumSize().height));
JPanel header = new JPanel();
header.setLayout(new GridBagLayout());
header.setBorder(BorderFactory.createEmptyBorder(4,8,4,4));
header.setBackground(LauncherFrame.COLOR_SELECTOR_OPTION);
add(header, BorderLayout.PAGE_START);
filterContents = new WatermarkTextField(resources.getString("launcher.packselector.filter.hotfix"), LauncherFrame.COLOR_BLUE_DARKER);
filterContents.setFont(resources.getFont(ResourceLoader.FONT_OPENSANS, 14));
filterContents.setBorder(new RoundBorder(LauncherFrame.COLOR_BUTTON_BLUE, 1, 8));
filterContents.setForeground(LauncherFrame.COLOR_BLUE);
filterContents.setBackground(LauncherFrame.COLOR_FORMELEMENT_INTERNAL);
filterContents.setSelectedTextColor(Color.black);
filterContents.setSelectionColor(LauncherFrame.COLOR_BUTTON_BLUE);
filterContents.setCaretColor(LauncherFrame.COLOR_BUTTON_BLUE);
filterContents.setColumns(20);
((AbstractDocument)filterContents.getDocument()).setDocumentFilter(new DocumentFilter() {
@Override
public void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException
{
if(fb.getDocument().getLength() + string.length() <= MAX_SEARCH_STRING)
{
fb.insertString(offset, string, attr);
}
}
@Override
public void remove(DocumentFilter.FilterBypass fb, int offset, int length) throws BadLocationException
{
fb.remove(offset, length);
}
@Override
public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)throws BadLocationException
{
int finalTextLength = (fb.getDocument().getLength() - length) + text.length();
if (finalTextLength > MAX_SEARCH_STRING)
text = text.substring(0, text.length() - (finalTextLength-MAX_SEARCH_STRING));
fb.replace(offset, length, text, attrs);
}
});
filterContents.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
detectFilterChanges();
}
@Override
public void removeUpdate(DocumentEvent e) {
detectFilterChanges();
}
@Override
public void changedUpdate(DocumentEvent e) {
detectFilterChanges();
}
});
header.add(filterContents, new GridBagConstraints(1,0,1,1,1,0,GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(3,0,3,0), 0, 12));
widgetList = new JPanel();
widgetList.setOpaque(false);
widgetList.setLayout(new GridBagLayout());
scrollPane = new JScrollPane(widgetList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setOpaque(false);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getViewport().setOpaque(false);
scrollPane.getVerticalScrollBar().setUI(new SimpleScrollbarUI(LauncherFrame.COLOR_SCROLL_TRACK, LauncherFrame.COLOR_SCROLL_THUMB));
scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(10, 10));
scrollPane.getVerticalScrollBar().setUnitIncrement(12);
add(scrollPane, BorderLayout.CENTER);
widgetList.add(Box.createHorizontalStrut(294), new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,0,0,0),0,0));
widgetList.add(Box.createGlue(), new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0,0,0,0), 0,0));
}
@Override
public void clear() {
allModpacks.clear();
rebuildUI();
}
@Override
public void replaceModpackInContainer(ModpackModel modpack) {
if (allModpacks.containsKey(modpack.getName()))
addModpackInternal(modpack);
}
@Override
public void addModpackToContainer(ModpackModel modpack) {
setTintActive(true);
addModpackInternal(modpack);
}
protected void addModpackInternal(ModpackModel modpack) {
final ModpackWidget widget = new ModpackWidget(resources, modpack, iconRepo.startImageJob(modpack));
if (modpack.hasRecommendedUpdate()) {
widget.setToolTipText(resources.getString("launcher.packselector.updatetip"));
}
widget.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() instanceof ModpackWidget)
selectWidget((ModpackWidget)e.getSource());
}
});
if (widget.getModpack().isSelected()) {
selectedWidget = widget;
}
if (allModpacks.containsKey(modpack.getName()) && allModpacks.get(modpack.getName()).isSelected())
selectedWidget = widget;
allModpacks.put(modpack.getName(), widget);
rebuildUI();
if (selectedWidget != null) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
if (widget == selectedWidget)
selectWidget(widget);
else
selectedWidget.scrollRectToVisible(new Rectangle(selectedWidget.getSize()));
}
});
}
}
@Override
public void refreshComplete() {
setTintActive(false);
if (findMoreWidget.getWidgetData().equals(resources.getString("launcher.packselector.api"))) {
if (allModpacks.size() == 0) {
findMoreWidget.setWidgetData(resources.getString("launcher.packselector.badapi"));
findMoreUrl = "http://www.technicpack.net/";
} else {
for(ModpackWidget widget : allModpacks.values()) {
findMoreUrl = widget.getModpack().getWebSite();
break;
}
}
}
if (selectedWidget == null || selectedWidget.getModpack() == null || !allModpacks.containsKey(selectedWidget.getModpack().getName())) {
java.util.List<ModpackWidget> sortedPacks = new LinkedList<ModpackWidget>();
sortedPacks.addAll(allModpacks.values());
Collections.sort(sortedPacks, new Comparator<ModpackWidget>() {
@Override
public int compare(ModpackWidget o1, ModpackWidget o2) {
int priorityCompare = (new Integer(o2.getModpack().getPriority())).compareTo(new Integer(o1.getModpack().getPriority()));
if (priorityCompare != 0)
return priorityCompare;
else if (o1.getModpack().getDisplayName() == null && o2.getModpack().getDisplayName() == null)
return 0;
else if (o1.getModpack().getDisplayName() == null)
return -1;
else if (o2.getModpack().getDisplayName() == null)
return 1;
else
return o1.getModpack().getDisplayName().compareToIgnoreCase(o2.getModpack().getDisplayName());
}
});
if (sortedPacks.size() > 0) {
selectWidget(sortedPacks.get(0));
}
}
}
protected void selectWidget(ModpackWidget widget) {
if (selectedWidget != null)
selectedWidget.setIsSelected(false);
selectedWidget = widget;
selectedWidget.setIsSelected(true);
selectedWidget.getModpack().select();
selectedWidget.scrollRectToVisible(new Rectangle(selectedWidget.getSize()));
if (modpackInfoPanel != null)
modpackInfoPanel.setModpack(widget.getModpack());
final ModpackWidget refreshWidget = selectedWidget;
Thread thread = new Thread("Modpack redownload " + selectedWidget.getModpack().getDisplayName()) {
@Override
public void run() {
try {
PlatformPackInfo updatedInfo = platformApi.getPlatformPackInfo(refreshWidget.getModpack().getName());
PackInfo infoToUse = updatedInfo;
if (updatedInfo != null && updatedInfo.hasSolder()) {
try {
ISolderPackApi solderPack = solderApi.getSolderPack(updatedInfo.getSolder(), updatedInfo.getName(), solderApi.getMirrorUrl(updatedInfo.getSolder()));
infoToUse = new CombinedPackInfo(solderPack.getPackInfo(), updatedInfo);
} catch (RestfulAPIException ex) {
}
}
if (infoToUse != null)
refreshWidget.getModpack().setPackInfo(infoToUse);
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
if (modpackInfoPanel != null)
modpackInfoPanel.setModpackIfSame(refreshWidget.getModpack());
if (refreshWidget.getModpack().hasRecommendedUpdate()) {
refreshWidget.setToolTipText(resources.getString("launcher.packselector.updatetip"));
} else {
refreshWidget.setToolTipText(null);
}
iconRepo.refreshRetry(refreshWidget.getModpack());
refreshWidget.updateFromPack(iconRepo.startImageJob(refreshWidget.getModpack()));
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
revalidate();
repaint();
}
});
}
});
} catch (RestfulAPIException ex) {
ex.printStackTrace();
return;
}
}
};
thread.start();
}
protected void rebuildUI() {
if (!EventQueue.isDispatchThread()) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
rebuildUI();
}
});
return;
}
GridBagConstraints constraints = new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0,0,0,0), 0,0);
java.util.List<ModpackWidget> sortedPacks = new LinkedList<ModpackWidget>();
sortedPacks.addAll(allModpacks.values());
Collections.sort(sortedPacks, new Comparator<ModpackWidget>() {
@Override
public int compare(ModpackWidget o1, ModpackWidget o2) {
int priorityCompare = (new Integer(o2.getModpack().getPriority())).compareTo(new Integer(o1.getModpack().getPriority()));
if (priorityCompare != 0)
return priorityCompare;
else if (o1.getModpack().getDisplayName() == null && o2.getModpack().getDisplayName() == null)
return 0;
else if (o1.getModpack().getDisplayName() == null)
return -1;
else if (o2.getModpack().getDisplayName() == null)
return 1;
else
return o1.getModpack().getDisplayName().compareToIgnoreCase(o2.getModpack().getDisplayName());
}
});
widgetList.removeAll();
for(ModpackWidget sortedPack : sortedPacks) {
widgetList.add(sortedPack, constraints);
constraints.gridy++;
}
if (filterContents.getText().length() >= 3) {
widgetList.add(findMoreWidget, constraints);
}
widgetList.add(Box.createHorizontalStrut(294), constraints);
constraints.gridy++;
constraints.weighty = 1.0;
widgetList.add(Box.createGlue(), constraints);
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
revalidate();
repaint();
}
});
}
@Override
public void userChanged(IUserType user) {
if (filterContents.getText().length() > 0)
filterContents.setText("");
else
detectFilterChanges();
if (user != null) {
ArrayList<IPackSource> sources = new ArrayList<IPackSource>(1);
sources.add(technicSolder);
defaultPacks.addPassthroughContainer(this);
packLoader.createRepositoryLoadJob(defaultPacks, sources, null, true);
}
}
public void forceRefresh() {
lastFilterContents = "THIS IS A TERRIBLE HACK I'M BASICALLY FORCING A REFRESH BUT WITHOUT DOING ANY WORK";
defaultPacks.clear();
detectFilterChanges();
ArrayList<IPackSource> sources = new ArrayList<IPackSource>(1);
sources.add(technicSolder);
packLoader.createRepositoryLoadJob(defaultPacks, sources, null, true);
}
public void setFilter(String text) {
filterContents.setText(text);
detectFilterChanges();
if (this.launcherFrame != null)
this.launcherFrame.selectTab("modpacks");
}
protected void detectFilterChanges() {
cancelJob();
if (filterContents.getText().length() >= 3) {
loadNewJob(filterContents.getText());
} else if (lastFilterContents.length() >= 3) {
clear();
defaultPacks.addPassthroughContainer(this);
for(ModpackModel modpack : defaultPacks.getModpacks()) {
addModpackToContainer(modpack);
}
refreshComplete();
}
lastFilterContents = filterContents.getText();
}
protected boolean isEncodedSlug(String slug) {
try {
URLEncoder.encode(URLDecoder.decode(slug, "UTF-8"), "UTF-8").equals(slug);
return true;
} catch (UnsupportedEncodingException ex) {
return false;
} catch (RuntimeException ex) {
//Apparently the encoder/decoder indicate bad input by THROWING RUNTIME EXCEPTIONS
//<3 java
return false;
}
}
private void loadNewJob(final String searchText) {
setTintActive(true);
defaultPacks.removePassthroughContainer(this);
currentSearchTimer = new Timer(500, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String localSearchTag = searchText;
String localSearchUrl = searchText;
if (!localSearchUrl.startsWith("http://") && !localSearchUrl.startsWith("https://"))
localSearchUrl = "http://" + localSearchTag;
try {
URI uri = new URI(localSearchUrl);
String host = uri.getHost();
String scheme = uri.getScheme();
if (host != null && scheme != null && (scheme.equals("http") || scheme.equals("https")) && (host.equals("www.technicpack.net") || host.equals("technicpack.net") || host.equals("api.technicpack.net"))) {
String path = uri.getPath();
if (path.startsWith("/"))
path = path.substring(1);
if (path.endsWith("/"))
path = path.substring(0, path.length()-1);
String[] fragments = path.split("/");
if ((fragments.length == 2 && fragments[0].equals("modpack")) || (fragments.length == 3 && fragments[0].equals("api") && fragments[1].equals("modpack"))) {
String slug = fragments[fragments.length-1];
Matcher siteMatcher = siteRegex.matcher(slug);
if (siteMatcher.find()) {
slug = siteMatcher.group(1);
}
Matcher slugMatcher = slugRegex.matcher(slug);
if (slugMatcher.find()) {
findMoreUrl = localSearchUrl;
findMoreWidget.setWidgetData(resources.getString("launcher.packselector.api"));
ArrayList<IPackSource> source = new ArrayList<IPackSource>(1);
source.add(new SinglePlatformSource(platformApi, solderApi, slug));
currentLoadJob = packLoader.createRepositoryLoadJob(ModpackSelector.this, source, null, false);
return;
}
}
}
} catch (URISyntaxException ex) {
//It wasn't a valid URI which is actually fine.
}
String encodedSearch = filterContents.getText();
try {
encodedSearch = URLEncoder.encode(encodedSearch, "UTF-8");
} catch (UnsupportedEncodingException ex) {}
findMoreUrl = "http://www.technicpack.net/modpacks?q="+encodedSearch;
findMoreWidget.setWidgetData(resources.getString("launcher.packselector.more"));
ArrayList<IPackSource> sources = new ArrayList<IPackSource>(2);
sources.add(new NameFilterPackSource(defaultPacks, localSearchTag));
sources.add(new SearchResultPackSource(platformSearchApi, localSearchTag));
currentLoadJob = packLoader.createRepositoryLoadJob(ModpackSelector.this, sources, null, false);
}
});
currentSearchTimer.setRepeats(false);
currentSearchTimer.start();
}
private void cancelJob() {
if (currentLoadJob != null)
currentLoadJob.cancel();
if (currentSearchTimer != null) {
currentSearchTimer.stop();
}
}
@Override
public void relocalize(ResourceLoader loader) {
this.resources = loader;
this.resources.registerResource(this);
this.setOverIcon(resources.getIcon("loader.gif"));
this.setTintActive(true);
//Wipe controls
removeAll();
this.setLayout(null);
initComponents();
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
invalidate();
repaint();
}
});
}
}